﻿//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Reflection;
using King.Extensions;

namespace King.Reflection {

    public interface ICustomAttributeProviderEx : ICustomAttributeProvider {
        ICustomAttributeProvider ReflectionInfo { get; }
    }
    public static partial class Extensions {

        // customAttributeProvider
        public static ICustomAttributeProvider GetReflectionInfo(this ICustomAttributeProvider provider) {
            if (provider is ICustomAttributeProviderEx)
                return ((ICustomAttributeProviderEx)provider).ReflectionInfo;
            return provider;
        }
        public static Assembly GetAssembly(this ICustomAttributeProvider provider) {
            if (provider == null)
                throw new ArgumentNullException();
            provider = provider.GetReflectionInfo();

            if (provider is MemberInfo)
                if (provider is Type)
                    return ((Type)provider).Assembly;
                else
                    return ((MemberInfo)provider).DeclaringType.Assembly;
            else if (provider is ParameterInfo)
                return ((ParameterInfo)provider).Member.DeclaringType.Assembly;
            else if (provider is Module)
                return ((Module)provider).Assembly;
            else if (provider is Assembly)
                return (Assembly)provider;

            throw new ArgumentException();
        }
        public static string GetName(this ICustomAttributeProvider provider) {
            provider = provider.GetReflectionInfo();

            if (provider is MemberInfo)
                return ((MemberInfo)provider).Name;
            if (provider is ParameterInfo)
                return ((ParameterInfo)provider).Name;
            if (provider is Assembly)
                return ((Assembly)provider).GetName().Name;
            if (provider is Module)
                return ((Module)provider).Name;

            throw new NotSupportedException();
        }

        // attributes by name
        public static TResult GetAttribute<T, TResult>(this ICustomAttributeProvider source, Func<T, TResult> projection) {
            var attribute = source.GetAttribute<T>();
            if (attribute == null)
                return default(TResult);
            return projection(attribute);
        }

        // attribute argument value by name
        public static object GetAttributeArgument(this ICustomAttributeProvider target, string name) {
            return target.GetAttributeArgument(name, typeof(object));
        }
        public static T GetAttributeArgument<T>(this ICustomAttributeProvider target, string name) {
            var result = target.GetAttributeArgument(name, typeof(T));
            if (result == null)
                return default(T);
            return (T)result;
        }
        public static object GetAttributeArgument(this ICustomAttributeProvider target, string name, Type type) {
            var order =
               (from attribute in target.GetAttributeData()
                from argument in attribute.Arguments
                where argument.Name == name && type.IsAssignableFrom(argument.Type)
                select argument.Value);
            return order.OnlyOneOrDefault();
        }

        // attribute metadata
        public static CustomAttributeData GetCustomAttributeData(this ICustomAttributeProvider provider, Type type) {
            var result =
                from o in provider.GetCustomAttributeData()
                let declaringType = o.Constructor.DeclaringType
                where type == declaringType || declaringType.IsSubclassOf(type)
                select o;

            return result.SingleOrDefault();
        }
        public static CustomAttributeData GetCustomAttributeData<T>(this ICustomAttributeProvider provider) {
            return provider.GetCustomAttributeData(typeof(T));
        }
        public static IEnumerable<CustomAttributeData> GetCustomAttributeData(this ICustomAttributeProvider provider) {
            provider = provider.GetReflectionInfo();

            if (provider is MemberInfo)
                return System.Reflection.CustomAttributeData.GetCustomAttributes((MemberInfo)provider);
            if (provider is ParameterInfo)
                return System.Reflection.CustomAttributeData.GetCustomAttributes((ParameterInfo)provider);
            if (provider is Assembly)
                return System.Reflection.CustomAttributeData.GetCustomAttributes((Assembly)provider);
            if (provider is Module)
                return System.Reflection.CustomAttributeData.GetCustomAttributes((Module)provider);

            throw new NotSupportedException();
        }

        // attribute metadata deserialization
        public static object Deserialize(this CustomAttributeTypedArgument customAttributeTypedArgument) {
            var o = customAttributeTypedArgument;

            if (o.ArgumentType.IsEnum)
                return Enum.ToObject(o.ArgumentType, o.Value);

            if (o.ArgumentType.IsArray) {
                var elements = ((IEnumerable)o.Value).Cast<CustomAttributeTypedArgument>().Select(x => x.Deserialize()).ToArray();
                var array = Array.CreateInstance(o.ArgumentType.GetElementType(), elements.Length);
                Array.Copy(elements, array, elements.Length);
                return array;
            }

            return o.Value;
        }
        public static T Deserialize<T>(this CustomAttributeTypedArgument customAttributeTypedArgument) {
            return (T)customAttributeTypedArgument.Deserialize();
        }
        public static object Deserialize(this CustomAttributeData customAttributeData) {
            var o = customAttributeData;

            var arguments = o.ConstructorArguments.Select(x => x.Deserialize()).ToArray();
            var result = o.Constructor.Invoke(arguments);

            foreach (var n in o.NamedArguments) {
                var value = n.TypedValue.Deserialize();
                var propertyInfo = n.MemberInfo as PropertyInfo;
                var fieldInfo = n.MemberInfo as FieldInfo;
                if (propertyInfo != null)
                    propertyInfo.SetValue(result, value, null);
                else
                    fieldInfo.SetValue(result, value);
            }

            return result;
        }
        public static T Deserialize<T>(this CustomAttributeData customAttributeData) {
            return (T)customAttributeData.Deserialize();
        }

        // attribute enhanced metadata
        public static AttributeData GetAttributeData(this ICustomAttributeProvider provider, Type type) {
            return new AttributeData(provider.GetCustomAttributeData(type));
        }
        public static AttributeData GetAttributeData<T>(this ICustomAttributeProvider provider) {
            return new AttributeData(provider.GetCustomAttributeData<T>());
        }
        public static IEnumerable<AttributeData> GetAttributeData(this ICustomAttributeProvider provider) {
            return provider.GetCustomAttributeData().Select(o => new AttributeData(o));
        }

        public static IEnumerable<T> GetAttributes<T>(this ICustomAttributeProvider source) {
            return source.GetAttributes(typeof(T)).Cast<T>();
        }
        public static object GetAttribute(this ICustomAttributeProvider source, Type type) {
            return source.GetAttributes(type).SingleOrDefault();
        }
        public static T GetAttribute<T>(this ICustomAttributeProvider source) {
            return source.GetAttributes<T>().SingleOrDefault();
        }

        public static bool IsDeclared(this ICustomAttributeProvider source, Type attributeType) {
            return source.IsDefined(attributeType, false);
        }
        public static bool IsDeclared<T>(this ICustomAttributeProvider source) {
            return source.IsDefined(typeof(T), false);
        }

        public static bool IsDefined(this ICustomAttributeProvider source, Type attributeType) {
            return source.IsDefined(attributeType, true);
        }
        public static bool IsDefined<T>(this ICustomAttributeProvider source) {
            return source.IsDefined(typeof(T), true);
        }
        public static IEnumerable<object> GetAttributes(this ICustomAttributeProvider source) {
            return source.GetAttributes(typeof(Attribute));
        }
        public static IEnumerable<object> GetAttributes(this ICustomAttributeProvider source, Type type) {
            IEnumerable<object> attributes = source.GetCustomAttributes(type, true);

            if (attributes == null)
                return Enumerable.Empty<object>();

            return attributes;
        }
    }

    [AttributeUsage(AttributeTargets.All)]
    public class OrderAttribute : Attribute {

        #region Private Data Members
        private int m_order;
        #endregion

        #region Constructor
        public OrderAttribute(int order) {
            m_order = order;
        }
        #endregion

        #region Public Members
        public int Order {
            get { return m_order; }
        }
        #endregion
    }

    [AttributeUsage(AttributeTargets.All)]
    public class GroupAttribute : Attribute {

        #region Private Data Members
        private int m_group;
        #endregion

        #region Constructor
        public GroupAttribute(int group) {
            m_group = group;
        }
        #endregion

        #region Public Members
        public int Group {
            get { return m_group; }
        }
        #endregion
    }

    public sealed class AttributeData {

        #region Private Data Members
        private AttributeInfo m_attributeInfo;
        private IEnumerable<AttributeArgument> m_attributeArguments;
        #endregion

        #region Constructor
        public AttributeData(CustomAttributeData data) {
            m_attributeInfo = AttributeInfo.GetAttributeInfo(data.Constructor.DeclaringType);

            var namedArguments =
               (from argument in data.NamedArguments
                join parameter in m_attributeInfo.NamedParameters
                    on new {
                        Token = argument.MemberInfo.MetadataToken,
                        Module = argument.MemberInfo.Module
                    } equals new {
                        Token = parameter.MemberInfo.MetadataToken,
                        Module = parameter.MemberInfo.Module
                    }
                let value = argument.TypedValue
                select new AttributeArgument(this, parameter, value)).ToArray();

            var constructorArguments =
               (from parameter in m_attributeInfo.ConstructorParameters
                where parameter.ParameterInfo.Member.MetadataToken == data.Constructor.MetadataToken
                let position = parameter.ParameterInfo.Position
                let value = data.ConstructorArguments[position]
                orderby position
                select new AttributeArgument(this, parameter, value)).ToArray();

            m_attributeArguments = constructorArguments.Concat(namedArguments).ToArray();
        }
        #endregion

        #region Public Members
        public AttributeInfo AttributeInfo {
            get { return m_attributeInfo; }
        }
        public IEnumerable<AttributeArgument> Arguments {
            get { return m_attributeArguments; }
        }
        public IEnumerable<AttributeArgument> ConstructorArguments {
            get { return m_attributeArguments.Where(o => o.Parameter.IsConstructorParameter); }
        }
        public IEnumerable<AttributeArgument> NamedArguments {
            get { return m_attributeArguments.Where(o => o.Parameter.IsNamedParameter); }
        }
        public object GetValue(string name) {
            var argument = Arguments.SingleOrDefault();
            if (argument == null)
                return null;

            return argument.Value;
        }
        public T GetValue<T>(string name) {
            var value = GetValue(name);
            if (value == null)
                return default(T);
            return (T)value;
        }
        #endregion

        #region AttributeInfo Members
        public Type Type {
            get { return AttributeInfo.Type; }
        }
        public AttributeInfo BaseAttribute {
            get { return AttributeInfo.BaseAttribute; }
        }
        public bool AllowMultipule {
            get { return AttributeInfo.AllowMultipule; }
        }
        public bool IsInherited {
            get { return AttributeInfo.IsInherited; }
        }
        public string Name {
            get { return AttributeInfo.Name; }
        }
        public IEnumerable<AttributeParameter> Parameters {
            get { return AttributeInfo.Parameters; }
        }
        public IEnumerable<AttributeParameter> ConstructorParameters {
            get { return AttributeInfo.ConstructorParameters; }
        }
        public IEnumerable<AttributeParameter> NamedParameters {
            get { return AttributeInfo.NamedParameters; }
        }
        #endregion
    }
    public sealed class AttributeArgument {

        #region Private Data Members
        private AttributeData m_attributeData;
        private AttributeParameter m_attributeParameter;
        private CustomAttributeTypedArgument m_value;
        private object m_runtimeValue;
        #endregion

        #region Constructor
        internal AttributeArgument(
            AttributeData attributeAttribute,
            AttributeParameter attributeParameter,
            CustomAttributeTypedArgument value) {
            m_attributeData = attributeAttribute;
            m_attributeParameter = attributeParameter;
            m_value = value;
        }
        #endregion

        #region Public Members
        public object Value {
            get {
                if (m_runtimeValue == null)
                    m_runtimeValue = m_value.Deserialize();
                return m_runtimeValue;
            }
        }
        public CustomAttributeTypedArgument ReflectionOnlyValue {
            get { return m_value; }
        }
        #endregion

        #region AttributeParameter Members
        public AttributeInfo AttributeInfo { get { return AttributeData.AttributeInfo; } }
        public AttributeData AttributeData { get { return m_attributeData; } }
        public AttributeParameter Parameter { get { return m_attributeParameter; } }
        public string Name { get { return Parameter.Name; } }
        public Type Type { get { return Parameter.Type; } }
        public MemberInfo MemberInfo { get { return Parameter.MemberInfo; } }
        public ParameterInfo ParameterInfo { get { return Parameter.ParameterInfo; } }
        public bool IsNamedParameter { get { return Parameter.IsNamedParameter; } }
        public bool IsConstructorParameter { get { return Parameter.IsConstructorParameter; } }
        public int Position { get { return Parameter.Position; } }
        #endregion
    }

    public sealed class AttributeInfo {

        #region Static Public Members
        public static object OrderByKey(AttributeInfo attributeInfo) {
            return OrderByKey(attributeInfo, Enumerable.Empty<Assembly>());
        }
        public static object OrderByKey(AttributeInfo attributeInfo, IEnumerable<Assembly> assemblyOrder) {
            var type = attributeInfo.Type;
            return new ComparableBuilder<AttributeInfo>(attributeInfo,
                o => o.Group != 0,
                o => assemblyOrder.FirstIndex(o.Type.Assembly) ?? int.MaxValue,
                o => o.Type.Assembly.FullName,
                o => o.Group,
                o => o.Order,
                o => o.Type.AssemblyQualifiedName);
        }
        public static object GroupByKey(AttributeInfo attributeInfo) {
            var type = attributeInfo.Type;
            var assembly = type.Assembly;
            var group = attributeInfo.Group;
            var allowMultipule = attributeInfo.AllowMultipule;

            if (group == 0)
                return allowMultipule;

            return new {
                Assembly = assembly,
                Group = group,
                AllowMultipule = allowMultipule ? new object() : null
            };
        }
        public static AttributeInfo GetAttributeInfo(Type type) {
            var result = GetAttributeInfoOrDefault(type);
            if (result == null)
                throw new ArgumentException();
            return result;
        }
        public static AttributeInfo GetAttributeInfoOrDefault(Type type) {
            if (type == null)
                return null;

            AttributeInfo cached;
            if (!s_attributeInfo.TryGetValue(type, out cached)) {
                var newValue = new AttributeInfo(type);
                lock (s_lock) {
                    if (!s_attributeInfo.TryGetValue(type, out cached)) {
                        s_attributeInfo[type] = cached = newValue;
                    }
                }
            }

            return cached;
        }
        #endregion

        #region Static Constructor
        static AttributeInfo() {
            s_attributeInfo = new Dictionary<Type, AttributeInfo>();
            s_lock = new object();
        }
        #endregion

        #region Static Private Members
        private static object s_lock;
        private static Dictionary<Type, AttributeInfo> s_attributeInfo;
        #endregion

        #region Private Data Members
        private Type m_type;
        private string m_name;

        private AttributeTargets m_validOn;
        private bool m_allowMultipule;
        private bool m_isInherited;

        private IEnumerable<AttributeParameter> m_namedParameters;
        private IEnumerable<AttributeParameter> m_ctorParameters;
        private AttributeInfo m_baseAttribute;

        private int? m_group;
        private int? m_order;
        private int m_depth;
        #endregion

        #region Constructor
        internal AttributeInfo(Type type) {
            m_type = type;
            m_depth = type.ToLinkList(o => o.BaseType).Count();

            var attributeUsage = type.GetCustomAttributeData<AttributeUsageAttribute>();
            if (attributeUsage != null) {
                m_allowMultipule = (bool)((bool?)attributeUsage.NamedArguments
                    .Where(o => o.MemberInfo.Name == "AllowMultiple")
                    .Select(o => o.TypedValue.Value)
                    .SingleOrDefault() ?? false);

                m_isInherited = (bool)((bool?)attributeUsage.NamedArguments
                    .Where(o => o.MemberInfo.Name == "Inherited")
                    .Select(o => o.TypedValue.Value)
                    .SingleOrDefault() ?? false);

                m_validOn = (AttributeTargets)attributeUsage.ConstructorArguments.First().Value;
            }

            var name = type.Name;
            m_name = name.EndsWith("Attribute") ? name.Remove(name.Length - "Attribute".Length) : name;

            var properties =
                (from o in m_type.Get<PropertyInfo>(isInstance: true, declaredOnly: true)
                where o.CanRead
                select o).ToArray();

            // named properties
            var namedProperties =
               (from o in properties
                where o.CanWrite
                select o).ToArray();

            // named fields
            var fields =
               (from o in m_type.Get<FieldInfo>(isInstance: true, declaredOnly: true)
                where o.IsPublic
                select o).ToArray();

            // named parameers
            m_namedParameters =
               (from o in fields.Union<MemberInfo>(namedProperties.Cast<MemberInfo>())
                select new AttributeParameter(this, o, null)).ToArray();

            // inherited parameters
            m_baseAttribute = GetAttributeInfoOrDefault(m_type.BaseType);
            if (m_baseAttribute != null)
                m_namedParameters = m_namedParameters.Concat(m_baseAttribute.NamedParameters).Distinct(o => o.Name).ToArray();

            // ctor parameters
            var ctorProperties =
                (from o in m_type.Get<PropertyInfo>(isInstance: true)
                where o.CanRead && !o.CanWrite
                select o).ToArray();
            var allCtorParameters = m_type.Get<ConstructorInfo>(isInstance: true, declaredOnly: true).SelectMany(p => p.GetParameters());
            m_ctorParameters =
               (from parameter in allCtorParameters
                join property in ctorProperties.Where(x => !x.CanWrite) on parameter.Name equals property.Name.ToLowerFirst()
                select new AttributeParameter(this, property, parameter)).ToArray();
        }
        #endregion

        #region Internal Members
        internal int Group {
            get {
                if (m_group == null)
                    m_group = m_type.GetAttributeArgument<int>("Group");
                return m_group.Value;
            }
        }
        internal int Order {
            get {
                if (m_order == null)
                    m_order = m_type.GetAttributeArgument<int>("Order");
                return m_order.Value;
            }
        }
        internal int Depth {
            get { return m_depth; }
        }
        #endregion

        #region Public Members
        public Type Type { get { return m_type; } }
        public string Name { get { return m_name; } }
        public AttributeInfo BaseAttribute { get { return m_baseAttribute; } }
        public bool AllowMultipule { get { return m_allowMultipule; } }
        public bool IsInherited { get { return m_isInherited; } }
        public IEnumerable<AttributeParameter> Parameters {
            get { return ConstructorParameters.Concat(NamedParameters).ToArray(); }
        }
        public IEnumerable<AttributeParameter> ConstructorParameters {
            get { return m_ctorParameters; }
        }
        public IEnumerable<AttributeParameter> NamedParameters {
            get { return m_namedParameters; }
        }
        #endregion

        #region Object Overrides
        public override string ToString() {
            return Name;
        }
        #endregion
    }
    public sealed class AttributeParameter {

        #region Static Public Members
        public static object OrderByKey(AttributeParameter attributeParameter) {
            return new ComparableBuilder<AttributeParameter>(attributeParameter,
                o => !o.IsConstructorParameter,
                o => o.Attribute.Depth,
                o => o.IsConstructorParameter ? o.Position : o.Order,
                o => o.Name);
        }
        #endregion

        #region Private Data Members
        private string m_name;
        private ParameterInfo m_parameterInfo;
        private MemberInfo m_memberInfo;
        private bool m_isConstructorParameter;
        private Type m_type;
        private AttributeInfo m_attribute;
        private int m_position;
        private int m_order;
        #endregion

        #region Constructor
        internal AttributeParameter(AttributeInfo attribute, MemberInfo memberInfo, ParameterInfo parameterInfo) {

            m_attribute = attribute;
            m_parameterInfo = parameterInfo;
            m_memberInfo = memberInfo;
            m_type = memberInfo.GetPropertyOrFieldType();
            m_name = memberInfo.Name;
            m_order = memberInfo.GetAttributeArgument<int>("Order");

            if (ParameterInfo != null) {
                m_isConstructorParameter = true;
                m_position = ParameterInfo.Position;

                if (m_order > 0 && m_position + 1 != m_order)
                    throw new ArgumentException();
            }
        }
        #endregion

        #region Internal Members
        internal int Order {
            get { return m_order; }
        }
        #endregion

        #region Public Members
        public AttributeInfo Attribute { get { return m_attribute; } }
        public Type Type { get { return m_type; } }
        public string Name { get { return m_name; } }
        public MemberInfo MemberInfo { get { return m_memberInfo; } }
        public ParameterInfo ParameterInfo { get { return m_parameterInfo; } }
        public bool IsNamedParameter { get { return !m_isConstructorParameter; } }
        public bool IsConstructorParameter { get { return m_isConstructorParameter; } }
        public int Position { get { return m_position; } }
        #endregion

        #region Object Overrides
        public sealed override string ToString() {
            return Attribute.Name + "." + Name;
        }
        #endregion
    }

    public sealed class ComparableBuilder<T> : IComparable {

        #region Private Data Members
        private T m_source;
        private IEnumerable<Func<T, IComparable>> m_orders;
        #endregion

        #region Constructor
        public ComparableBuilder(T source, params Func<T, IComparable>[] orders) {
            m_source = source;
            m_orders = orders;
        }
        #endregion

        #region IComparable Members
        public int CompareTo(object target) {
            if (target == null)
                throw new InvalidOperationException();

            if (target.GetType() != GetType())
                throw new InvalidOperationException();

            return Comparable.CompareTo(m_source, ((ComparableBuilder<T>)target).m_source, m_orders);
        }
        #endregion
    }
    public static class Comparable {
        public static int CompareTo<T>(T source, T target, IEnumerable<Func<T, IComparable>> orders) {
            if (target == null)
                throw new InvalidOperationException();

            if (target.GetType() != source.GetType())
                throw new InvalidOperationException();

            var lhs = orders.Select(o => o(source));
            var rhs = orders.Select(o => o(target));
            var zip = lhs.Zipper(rhs, (Lhs, Rhs) => new { Lhs, Rhs });
            foreach (var o in zip) {
                var compare = o.Lhs.CompareTo(o.Rhs);
                if (compare != 0)
                    return compare;
            }

            return 0;
        }
    }

    public class AnyEqualityComparer : IEqualityComparer {

        #region Static Members
        public static IEqualityComparer<IEnumerable<T>> GetEnumerableComparer<T>() {
            return new AnyEqualityComparer<IEnumerable<T>>(
                o => o.Aggregate(0, (a, x) => a ^ x.GetHashCode()),
                (x, y) => x.IsEqualTo(y));
        }
        #endregion

        #region Private Members
        private Func<object, int> m_getHashCode;
        private Func<object, object, bool> m_isEqualTo;
        #endregion

        #region Constructor
        public AnyEqualityComparer(Func<object, int> getHashCode, Func<object, object, bool> isEqualTo) {
            m_getHashCode = getHashCode;
            m_isEqualTo = isEqualTo;
        }
        #endregion

        #region IEqualityComparer Members
        public int GetHashCode(object obj) {
            return m_getHashCode(obj);
        }
        public new bool Equals(object x, object y) {
            return m_isEqualTo(x, y);
        }
        #endregion
    }
    public class AnyEqualityComparer<T> : AnyEqualityComparer, IEqualityComparer<T> {

        #region Constructor
        public AnyEqualityComparer(Func<T, int> getHashCode, Func<T, T, bool> isEqualTo)
            : base(o => getHashCode((T)o), (x, y) => isEqualTo((T)x, (T)y)) {
        }
        #endregion

        #region IEqualityComparer<T> Members
        public bool Equals(T x, T y) {
            return ((IEqualityComparer)this).Equals(x, y);
        }
        public int GetHashCode(T obj) {
            return ((IEqualityComparer)this).GetHashCode(obj);
        }
        #endregion
    }
}